/*
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.hotswap.agent.plugin.resteasy;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.ServletContext;
import javax.ws.rs.Path;

import org.hotswap.agent.command.MergeableCommand;
import org.hotswap.agent.logging.AgentLogger;
import org.hotswap.agent.logging.AgentLogger.Level;
import org.jboss.resteasy.core.ResourceInvoker;
import org.jboss.resteasy.core.ResourceMethodRegistry;
import org.jboss.resteasy.core.registry.RootClassNode;
import org.jboss.resteasy.core.registry.RootNode;
import org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher;
import org.jboss.resteasy.spi.Registry;

/**
 * Resteasy registers methods based the class path. Since the redefined class
 * may be in a different path or the method we need to iterate over known
 * bounded methods, reconstruct the class/method path and remove each method.
 * 
 * Since the class/method path declarations may have not changed, we use the
 * registry removeRegistrations method also.
 * 
 * The "original" class, does not seem to do the above trick...
 * 
 * @author alpapad@gmail.com
 */
public class RefreshRegistryCommand extends MergeableCommand {

	private static AgentLogger LOGGER = AgentLogger.getLogger(RefreshRegistryCommand.class);

	private ClassLoader classLoader;

	private ServletContext context;

	private String className;

	private ServletContainerDispatcher servletContainerDispatcher;
	
	private Class<?> original;

	public void setupCmd(ClassLoader classLoader, Object context, Object servletContainerDispatcher, String className, Class<?> original) {
		this.classLoader = classLoader;
		this.context = (ServletContext) context;
		this.servletContainerDispatcher = (ServletContainerDispatcher) servletContainerDispatcher;
		this.className = className;
		this.original = original;
	}

	@Override
	public void executeCommand() {
		ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();

		LOGGER.debug("Re-Loading class: {} , {} , {}", className, oldClassLoader, classLoader);
		
		Thread.currentThread().setContextClassLoader(classLoader);
		try {
			Registry registry = (Registry) context.getAttribute(Registry.class.getName());
			if (registry == null) {
				registry = servletContainerDispatcher.getDispatcher().getRegistry();
			}
			//Does original actually represent the... original class????
			if(original != null) {
				registry.removeRegistrations(original);
			}
			
			Class<?> c = classLoader.loadClass(className);

			//Remove all matching registrations (between old and new class..)
			registry.removeRegistrations(c);

			//Iterate over all known methods for this className
			if (registry instanceof ResourceMethodRegistry) {
				ResourceMethodRegistry rm = ResourceMethodRegistry.class.cast(registry);
				Map<String, List<ResourceInvoker>> bounded = rm.getBounded();
				for (Entry<String, List<ResourceInvoker>> e : bounded.entrySet()) {
					LOGGER.debug("Examining {}", e.getKey());
					for (ResourceInvoker r : e.getValue()) {
						if(LOGGER.isLevelEnabled(Level.DEBUG)){
							LOGGER.debug("Examining {} for method {} in class {}", e.getKey(), r.getMethod().getName(),
									r.getMethod().getDeclaringClass());
						}
						if (r.getMethod().getDeclaringClass().getName().equals(className)) {
							removeRegistration(rm, e.getKey(), r.getMethod());
						}
					}
				}
			}

			//Add the new resource
			registry.addPerRequestResource(c);
		} catch (Exception e) {
			LOGGER.error("Could not reload rest class {}", e, className);
		} finally {
			Thread.currentThread().setContextClassLoader(oldClassLoader);
		}
	}

	@SuppressWarnings("unchecked")
	private <T> T get(ResourceMethodRegistry rm, String field) {
		Class<?> c;
		try {
			c = classLoader.loadClass(ResourceMethodRegistry.class.getName());
			Field f = c.getField(field);
			return (T) f.get(rm);
		} catch (ClassNotFoundException | NoSuchFieldException | SecurityException | IllegalArgumentException
				| IllegalAccessException e) {
			LOGGER.error("Could not get field {}", e, field);
		}

		return null;
	}

	private void removeRegistration(ResourceMethodRegistry rm, String path, Method method) {
		try {
			if (rm.isWiderMatching()) {
				RootNode rootNode = get(rm, "rootNode");
				rootNode.removeBinding(path, method);
			} else {
				String methodpath = method.getAnnotation(Path.class).value();
				String classExpression = path.replace(methodpath, "");
				if (classExpression.endsWith("/")) {
					classExpression.substring(0, classExpression.length() - 1);
				}
				RootClassNode root = get(rm, "root");
				root.removeBinding(classExpression, path, method);
			}
		} catch (Exception e) {
			LOGGER.error("Could not remove method registration from path {}, {}", e, path, method);
		}
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((classLoader == null) ? 0 : classLoader.hashCode());
		result = prime * result + ((className == null) ? 0 : className.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		RefreshRegistryCommand other = (RefreshRegistryCommand) obj;
		if (classLoader == null) {
			if (other.classLoader != null)
				return false;
		} else if (!classLoader.equals(other.classLoader))
			return false;
		if (className == null) {
			if (other.className != null)
				return false;
		} else if (!className.equals(other.className))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "RefreshRegistryCommand [classLoader=" + classLoader + ", className=" + className + "]";
	}
}